前言
”更新视图但不重新请求页面“ 是前端路由原理的核心之一,目前在浏览器环境中这一功能的实现主要有两种方式
- 利用 URL 中的 hash(#)
- 利用 History interface 在 HTML5 中新增的方法
下面看看在 Vue-router 中是如何通过这两种方式实现前端路由
匹配模式
创建 VueRouter 的实例对象时,mode 以构造函数参数的形式传入
实例化 VueRouter
1 | export default class VueRouter { |
初始化路由
1 | init(app: any /* Vue component instance */) { |
HashHistory
http://www.example.com/index.html#print
# 符号本身以及它后面的字符称为 hash,可通过 window.location.hash
属性读取
特点:
- hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,它是用来指导浏览器动作的,对服务端完全无用。因此,改变 hash 不会重新加载页面
- 可以为 hash 的改变添加监听事件
window.addEventListener('hashchange', funcRef, false)
- 每一次改变 hash(window.location.hash),都会在浏览器的访问历史中增加一个记录
push()
跳转的方式会判断是否需要滚动,需要,将会使用 pushState 改变路由;否则对 window 的 hash 进行直接赋值
hash 的改变会自动添加到浏览器的访问历史记录中
1 | push (location: RawLocation, onComplete?: Function, onAbort?: Function) { |
实现视图更新
先上结论,从设置路由改变到视图更新的流程
$router.push() --> HashHistory.push() --> History.transitionTo() -->History.updateRoute() --> {app._route = route} --> vm.render()
我们现在看父类 History 中 transitionTo() 的实现
1 | transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { |
当路由变化时,调用了 this.cb
方法, this.cb
方法是通过 History.listen(cb) 进行设置的。让我们看看 cb 在哪里传入
1 | init (app: any /* Vue component instance */) { |
可以看到,在路由初始化的函数中,会传入修改app._route = route
的 cb ,有什么作用呢??在 install.js 里面会全局注册一个混合,给所有 Vue实例的 beforeCreate 钩子定义了响应式的 _route属性,所以,当 _route 改变时,会自动调用 Vue 实例的 render() 方法,更新视图
replace()
如果浏览器支持 HTML5 特性,将会使用 replaceState 改变路由;否则调用 window.location.replace 方法将路由进行替换
不会把新路由添加到浏览器访问历史的栈顶,而是替换掉当前的路由
1 | replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { |
setupListeners()
监听地址栏
用户除了使用push与replace
改变路由,而可以直接在浏览器地址栏中输入改变路由,所以 VueRouter 还需要能监听浏览器地址栏中路由的变化,并具有与通过代码调用相同的响应行为
在 HashHistory 中通过setupListeners
方法实现
跳转时会判断是否需要滚动行为,如果需要,则监听浏览器 popstate 事件,否则监听浏览器 hashchange 事件,调用 replaceHash 函数,即在浏览器地址栏中直接输入路由相当于代码调用了 replace() 方法
1 | setupListeners () { |
HTML5History
History interface 是浏览器历史记录栈提供的接口,通过 back() ,forward() ,go() 等方法,我们可以读取浏览器历史记录栈的信息,进行各种跳转操作
从 HTML5 开始,History interface 提供了两个新的方法:pushState() ,replaceState() 使得我们可以对浏览器历史记录栈进行修改
1 | /** |
这两个方法有个共同的特点:当调用它们修改浏览器历史记录栈时,虽然当前 URL 改变了,但浏览器不会立刻发送请求该 URL,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础
push() 与 replace()
VueRouter 中的实现:会发现代码结构以及更新视图的逻辑与 hash 模式基本类似,区别是:将 window.location.hash window.location.replace()
改为调用 window.history.pushState() window.history.replaceState()
1 | push (location: RawLocation, onComplete?: Function, onAbort?: Function) { |
监听地址栏
在 HTML5History 中添加对修改浏览器地址栏 URL 的监听是直接在构造函数中执行的,注意只监听 popstate 事件
1 | export class HTML5History extends History { |
handleScroll
处理滚动
1 | // 实现滚动的两个小办法 |
滚动行为:使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持在原先的滚动位置,就像重新加载页面那样。VueRouter 可以自定义路由切换时页面如何滚动
1 | export function handleScroll( |
supportsPushState
检查浏览器是否支持 HTML5 特性
HTML5History 用到了 HTML5 的特性,需要特定浏览器版本的支持,浏览器是否支持是通过变量 supportsPushState 来检查的:
1 | export const supportsPushState = |
总结
hash模式与 history模式都是通过浏览器接口实现,除此之外,VueRouter 还为非浏览器环境准备了一个 abstract模式,原理:用一个数组 stack 模拟浏览器历史记录栈的功能
两种模式的比较
history 模式的优点
如果不想要很丑的 hash,我们可以用路由的 history 模式 ——官方文档
在 MDN 的介绍中,是这样描述的:
在某种意义上,调用 pushState()
与 设置 window.location = "#foo"
类似,二者都会在当前页面创建并激活新的历史记录。但 pushState()
具有如下几条优点:
-
pushState()
设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,故只可设置与当前同文档的 URL -
pushState()
设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发记录添加到栈钟 -
pushState()
通过 stateObject 可以添加任意类型的数据到记录中;而 hash 只可添加短字符串 -
pushState()
可额外设置 title 属性供后续使用
注意 pushState()
绝对不会触发 hashchange
事件,即使新的URL与旧的URL仅哈希不同也是如此。
history 模式的一个问题
对于单页面应用来说,理想的使用场景是仅在进入应用时加载 index.html,后续的网络操作通过 Ajax 完成,不会根据 URL 重新请求页面。但是如果遇到特殊情况:用户直接在地址栏中输入并按回车,浏览器重启重新加载应用等
hash 模式仅改变 hash 部分的内容,而hash 部分不会包含在 HTTP 请求中。所以在 hash 模式下遇到特殊情况不会有问题
1 | http://oursite.com/#/user/id // 如重新请求只会发送http://oursite.com/ |
而history 模式会将整个 URL 重新发送请求,如果后端没有配置对应 /user/id 的路由处理,则会返回 404 错误
1 | http://oursite.com/user/id |
官方推荐的解决办法是:在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html
页面,这个页面就是你 app 依赖的页面。
同时,我们要给一个警告,因为这么做以后,你的服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html
文件。为了避免这种情况,你应该在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面
1 | const router = new VueRouter({ |
或者,如果你使用 Node.js 服务器,你可以用服务端路由匹配到来的 URL,并在没有匹配到路由的时候返回 404,以实现回退。更多详情可查阅官网